/*
 * GsmFunctions.java
 *
 * Created on 17. Januar 2007, 15:30
 *
 *  This file is part of JSmex.
 *
 *  JSmex is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  JSmex is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Foobar; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package jsmex.function;

import javax.swing.JOptionPane;
import jsmex.JSmexTools;
import jsmex.LogTracer;
import jsmex.cardservice.GsmCardService;
import jsmex.gui.ChangePinDialog;
import jsmex.gui.GsmGUI;
import jsmex.gui.PINDialog;
import opencard.core.terminal.CardTerminalException;
import opencard.core.terminal.ResponseAPDU;
import opencard.core.util.HexString;

/**
 *
 * @author Tobias Senger
 */
public class GsmFunctions {
    
    private GsmGUI gui = null;
    private GsmCardService cs = null;
    private LogTracer lt = null;
    private byte[] SRES = new byte[4];
    private byte[] Kc = new byte[8];
    private ResponseAPDU lastresponse = null;
    private PINDialog pd = null;
    
    
    /** Creates a new instance of GsmFunctions */
    public GsmFunctions(GsmCardService gsmcs, LogTracer ltracer) {
        this.cs = gsmcs;
        this.lt = ltracer;
        showGUI();
    }
    
    public boolean changePIN(int index) {
        
        lt.info("called method changePIN(index: "+Integer.toString(index)+").", this);
        
        ChangePinDialog cpd = new ChangePinDialog(gui,true);
        cpd.setVisible(true);
        String[] pins = cpd.getReturnStatus();
        if (pins==null) return false;
        
        ResponseAPDU resp = cs.changeCHV((byte) index, pins[0], pins[1]);
        
        if (resp.sw()==0x9000) return true;
        else return false;
    }
    
    /**Calls the RUN GSM-ALGORITHM command on the SIM.
     *
     * @param s String s contains a challenge.
     * @return Returns 'true' if comman was succesful.
     */
    public boolean runGSMAlgorithm(String s) {
        
        lt.info("called method runGSMAlgorithm(s: "+s+").", this);
        
        int vp = verifyPIN(1);
        if (vp==1) {
            JOptionPane.showMessageDialog(gui, "Wrong PIN!");
            return false;
        } else if (vp==2) {
            JOptionPane.showMessageDialog(gui, "Abort!");
            return false;
        }
        
        cs.selectDF(JSmexTools.parseHexString("7F 20"));
        
        byte[] rand = null;
        try { rand = JSmexTools.parseHexString(s); } catch (java.lang.NumberFormatException ex) {
            JOptionPane.showMessageDialog(null, "Please enter a valid 16 byte HexString!");
            return false;
        }
        
        if (rand.length<16) {
            JOptionPane.showMessageDialog(null, "Please enter a valid 16 byte HexString!");
            return false;
        }
        
        ResponseAPDU resp = cs.runGsmAlgorithm(rand);
        if (resp.sw()==0x9000) {
            System.arraycopy(resp.data(),0,SRES,0,4);
            System.arraycopy(resp.data(),4,Kc,0,8);
            return true;
        } else return false;
    }
    
    public String getSRES() {
        lt.info("called method getSRES(). returns: "+HexString.hexify(SRES), this);
        return HexString.hexify(SRES);
    }
    
    public String getKc() {
        lt.info("called method getKc(). returns: "+HexString.hexify(Kc), this);
        return HexString.hexify(Kc);
    }
    
    
    public int verifyPIN(int pinIndex) {
        
        lt.info("called method verifyPIN(pinIndex: "+Integer.toString(pinIndex)+").", this);
        
        if (cs.chvIsVerified(pinIndex)) return 0;
        else {
            if (pd == null) pd = new PINDialog();
            String pin = pd.getCHV(gui, pinIndex);
            if (pin==null) return 2;
            ResponseAPDU resp = cs.verifyCHV((byte)pinIndex, pin);
            
            if (resp.sw()==0x9000) return 0;
            else return 1;
        }
    }
    
    /** Select the DF Telecom file on the GSM card and checks for CHV1 if neccessary.
     *
     * @return ResponseAPDU from select EF ADN.
     */
    public ResponseAPDU selectPhonebook() {
        
        lt.info("called method selectPhonebook().", this);
        
        if (cs.getSelectedFID()[0]==(byte)0x6f && cs.getSelectedFID()[1] == (byte)0x3A)
            return cs.selectFID("6F 3A");
        
        else {
            
            int temp = cs.selectFID("3F 00").getByte(13);
            
            cs.selectFID("7F 10");
            
            if (cs.chvIsVerified(1) == false) return null;
            
            return cs.selectFID("6F 3A");
        }
        
    }   // selectPhonebook()
    
    public int getNumberOfPhonebookEntries() {
        
        lt.info("called getNumberOfPhonebookEntries()", this);
        
        ResponseAPDU rsp = selectPhonebook();
        int sizeADNRec = rsp.getByte(14);
        return ((rsp.getByte(2) << 8) | (rsp.getByte(3))) / sizeADNRec;
    }
    
    /** Reads an GSMPhonebookEntry at position index from the card and returns a
     * new GSMPhonebookEntry instance.
     * @param index The index/rec-no. of the entry.
     * @throws CardTerminalException Thrown in case of an error.
     * @return new GSMCardPhonebookEntry instance.
     */
    public PhonebookEntry getPhonebookEntry(int index) {
        
        lt.info("called getPhonebookEntry(index: "+Integer.toString(index)+").", this);
        
        byte length;
        int j, k;
        byte[] field;
        int lengthID;
        int numADNRec;
        int sizeADNRec;
        int lengthOfAlphaID;
        
        String tname, tnumber;
        ResponseAPDU rsp = null;
        byte[] response;
        
        rsp = selectPhonebook();
        
        sizeADNRec = rsp.getByte(14);
        numADNRec = ((rsp.getByte(2) << 8) | (rsp.getByte(3))) / sizeADNRec;
        lengthOfAlphaID = sizeADNRec - 14;
        
        if (index > numADNRec)
            return null;
        
        rsp = cs.readRecord((byte)index,(byte)sizeADNRec);
        
        lengthID = lengthOfAlphaID;
        
        response = rsp.data();
        
        field = new byte[lengthID];
        
        System.arraycopy(response, 0, field, 0, lengthID);
        
        tname = GSMCardUtils.decodeName(field);
        
        
        length = response[lengthID];
        
        if (length == (byte) 0xff)  {
            field = new byte[1];
            length = 1;
        } else
            field = new byte[length];
        
        System.arraycopy(response, lengthID + 1, field, 0, length);
        tnumber = GSMCardUtils.decodePhoneNumber(field);
        
        tname.trim();
        tnumber.trim();
        
        return new PhonebookEntry(tname, tnumber, index);
        
    }  // getGSMPhonebookEntry()
    
    /** Saves the GSMPhonebookEntry ent at the position index to the card.
     * @param ent The GSMPhonebookEntry to be saved.
     * @param index The position/rec-no. of the entry.
     *
     */
    public void savePhonebookEntry(PhonebookEntry ent, int index) {
        
        lt.info("called method savePhonebookEntry(PhonebookEntry ent, int index).", this);
        
        ResponseAPDU rsp = null;
        int i, length;
        int numADNRec;
        int sizeADNRec;
        int lengthOfAlphaID;
        
        rsp = selectPhonebook();
        
        sizeADNRec = rsp.getByte(14);
        numADNRec = ((rsp.getByte(2) << 8) | (rsp.getByte(3))) / sizeADNRec;
        lengthOfAlphaID = sizeADNRec - 14;
        
        
        if (index > numADNRec)
            return;
        
        byte [] Name = GSMCardUtils.codeName(ent.getName(), lengthOfAlphaID);
        byte [] Number = GSMCardUtils.codePhoneNumber(ent.getNumber());
        
        byte [] newData = new byte[sizeADNRec];
        
        for(i=0; i < newData.length; i++)
            newData[i] = (byte)0xff;
        
        if (Name != null)
            System.arraycopy(Name, 0, newData, 0, Name.length);
        
        if (Number != null)  {
            length = (char) Number.length;
            
            // Now set the number of bytes used by BCD coding
            newData[lengthOfAlphaID] = (byte) length;
            
            // now copy the number
            System.arraycopy(Number, 0, newData, lengthOfAlphaID + 1, length);
            
        }
        
        rsp = cs.updateRecord((byte)(index+1),newData);
        
        //       if (rsp.sw1() != (byte)0x90)
        //           throw new CardTerminalException("Error writing/updating phonebook entry!");
        
    }  // saveGSMPhonebookEntry()
    
    
    /**Show GSM-GUI
     *
     */
    public void showGUI() {
        lt.info("called method showGUI().", this);
        if (gui == null) gui = new GsmGUI(this);
        gui.setVisible(true);
    }
    
    /**Close GSM-GUI
     *
     */
    public void closeGUI() {
        lt.info("called method closeGUI().", this);
        gui.setVisible(false);
        gui.dispose();
        gui = null;
    }
    
    public String isPin1Enabled() {
        ResponseAPDU resp = cs.selectFID("3F 00");
        if ((resp.getByte(13)&128) == 128) {
            lt.info("called method isPin1Enabled(). returns 'true'.", this);
            return new String("false");
        } else {
            lt.info("called method isPin1Enabled(). returns 'false'.", this);
            return new String("true");
        }
    }
    
    public String getPin1Retries() {
        ResponseAPDU resp = cs.selectFID("3F 00");
        String retries = Integer.toString(resp.getByte(18)&15);
        lt.info("called method getPin1Retries(). returns: "+retries, this);
        return retries;
    }
    
    public String isPin1Blocked() {
        ResponseAPDU resp = cs.selectFID("3F 00");
        if ((resp.getByte(18)&15) == 0) {
            lt.info("called method isPin1Blocked(). returns 'true'.", this);
            return new String("true");
        } else {
            lt.info("called method isPin1Blocked(). returns 'false'.", this);
            return new String("false");
        }
    }
    
    public String isPin1Initialized() {
        ResponseAPDU resp = cs.selectFID("3F 00");
        if ((resp.getByte(18)&128)== 128) {
            lt.info("called method isPin1Initialized(). returns 'true'.", this);
            return new String("true");
        } else {
            lt.info("called method isPin1Initialized(). returns 'false'.", this);
            return new String("false");
        }
    }
    
    public String isPuk1Initialized() {
        ResponseAPDU resp = cs.selectFID("3F 00");
        if ((resp.getByte(19)&128)== 128) {
            lt.info("called method isPuk1Initialized(). returns 'true'.", this);
            return new String("true");
        } else {
            lt.info("called method isPuk1Initialized(). returns 'false'.", this);
            return new String("false");
        }
    }
    
    public String isPuk1Blocked() {
        ResponseAPDU resp = cs.selectFID("3F 00");
        if ((resp.getByte(19)&15) == 0) {
            lt.info("called method isPuk1Blocked(). returns 'true'.", this);
            return new String("true");
        } else {
            lt.info("called method isPuk1Blocked(). returns 'false'.", this);
            return new String("false");
        }
    }
    
    public String getPuk1Retries() {
        ResponseAPDU resp = cs.selectFID("3F 00");
        lt.info("called method getPuk1Retries(). returns: "+Integer.toString(resp.getByte(19)&15), this);
        return Integer.toString(resp.getByte(19)&15);
    }
    
    public String isPin2Initialized() {
        ResponseAPDU resp = cs.selectFID("3F 00");
        if ((resp.getByte(20)&128)== 128) {
            lt.info("called method isPin2Initialized(). returns 'true'.", this);
            return new String("true");
        } else {
            lt.info("called method isPin2Initialized(). returns 'false'.", this);
            return new String("false");
        }
    }
    
    public String isPin2Blocked() {
        ResponseAPDU resp = cs.selectFID("3F 00");
        if ((resp.getByte(20)&15) == 0) {
            lt.info("called method isPin2Blocked(). returns 'true'.", this);
            return new String("true");
        } else {
            lt.info("called method isPin2Blocked(). returns 'false'.", this);
            return new String("false");
        }
    }
    
    public String getPin2Retries() {
        ResponseAPDU resp = cs.selectFID("3F 00");
        lt.info("called method getPin2Retries(). returns:"+Integer.toString(resp.getByte(20)&15), this);
        return Integer.toString(resp.getByte(20)&15);
    }
    
    public String isPuk2Initialized() {
        ResponseAPDU resp = cs.selectFID("3F 00");
        if ((resp.getByte(21)&128)== 128) {
            lt.info("called method isPuk2Initialized(). returns 'true'.", this);
            return new String("true");
        } else {
            lt.info("called method isPuk2Initialized(). returns 'false'.", this);
            return new String("false");
        }
    }
    
    public String isPuk2Blocked() {
        ResponseAPDU resp = cs.selectFID("3F 00");
        if ((resp.getByte(21)&15) == 0) {
            lt.info("called method isPuk2Blocked(). returns 'true'.", this);
            return new String("true");
        } else {
            lt.info("called method isPuk2Blocked(). returns 'false'.", this);
            return new String("false");
        }
    }
    
    public String getPuk2Retries() {
        ResponseAPDU resp = cs.selectFID("3F 00");
        lt.info("called method getPuk2Retries(). returns: "+Integer.toString(resp.getByte(21)&15), this);
        return Integer.toString(resp.getByte(21)&15);
    }
    
    /**Returns a String which contains a decoded SMS with all infos.
     *
     * @param index The index specifies which SMS should be read.
     * @return
     */
    public String getSMS(int index) {
        lt.info("called method getSMS(index: "+Integer.toString(index)+").", this);
        ResponseAPDU resp = null;
        cs.selectMF();
        cs.selectDF(new byte[]{(byte)0x7F, (byte)0x10}); //DF TELECOM
        resp = cs.selectEF(new byte[]{(byte)0x6F, (byte)0x3C}); //EF SMS
        int recLength = resp.getByte(14);
        resp = cs.readRecord((byte)index,recLength);
        
        return decodeSMSRecord(resp.getBytes(), resp.getLength());
    }
    
    /**Returns a String array which contains all available SMS-Indexes
     *
     * @return
     */
    public String[] getSMSList() {
        
        lt.info("called method getSMSList().", this);
        
        ResponseAPDU resp = null;
        cs.selectMF();
        cs.selectDF(new byte[]{(byte)0x7F, (byte)0x10}); //DF TELECOM
        resp = cs.selectEF(new byte[]{(byte)0x6F, (byte)0x3C}); //EF SMS
        int sizeRec = resp.getByte(14);
        int numRec = ((resp.getByte(2) << 8) | (resp.getByte(3))) / sizeRec;
        
        String[] listItems = new String[numRec];
        
        for (int i=0; i<numRec; i++) {
            listItems[i] = "SMS "+Integer.toString(i+1);
        }
        
        return listItems;
    }
    
    /** This method decodes a SMS from the given byte array in RspAPDU.
     *  This metho was witten by Wolfgang Rankl. (http://www.wrankl.de)
     *
     * @param RspAPDU The byte array contains the repsonse data from a readRecord comman an EF_SMS.
     * @param LenRspAPDU This is the length of the response data.
     * @return Returns a String which contains the decoded SMS.
     */
    private String decodeSMSRecord(byte[] RspAPDU, int LenRspAPDU) {
        
        lt.info("called method decodeSMSRecord(byte[] RspAPDU, int LenRspAPDU).", this);
        
        int x, n, y, Bytebefore, Bitshift, LenRecord, EndOfSms = 0;
        int MessageStart, LenMessage;
        String b = new String();
        String c = new String();
        String s = new String("");
        LenRecord = LenRspAPDU - 2;   // length of the record = APDU length without SW
        //----- look for the end of the SMS
        //----- don't convert the unused characters (= 0xFF) at the end of the SMS
        for (n = LenRecord ; n > 0; n--) {
            EndOfSms = n;
            x = (0x000000FF & RspAPDU[n-1]);
            if (x != 0xFF) break;
        } // for
        x = (0x000000FF &  RspAPDU[0]);
        if (x == 0x00) {
            s = "unused record";
            return s;
        } // if
        else if (x == 0x01) s =  "SMS from net and read:      ";
        else if (x == 0x03) s =  "SMS from net and to read:   ";
        else if (x == 0x05) s =  "SMS already send to the net";
        else if (x == 0x07) s =  "SMS to send to the net";
        else s = "not specified  SMS status";
        if (x == 0x01 | x == 0x03) {               // the following decodes only a SMS
            //----- find the beginning of the SMS message
            MessageStart = 1;                        // consider SMS status byte
            x = (0x000000FF & RspAPDU[1]);     // length nof SMSC dialing number
            MessageStart = MessageStart + 1 + x;
            MessageStart = MessageStart + 1;        // control information
            x = RspAPDU[MessageStart];              // number of digits of the sender
            x = x / 2;                              // conversion: digits -> bytes
            x = x + 2;                              // add 2 byte for number of digits o
            MessageStart = MessageStart + x;
            MessageStart = MessageStart + 1 + 1;    // message type, message alphabet
            MessageStart = MessageStart + 7;        // time stamp
            x = (0x000000FF & RspAPDU[MessageStart]); // length of SMS message in
            LenMessage = x;
            //----- copy SMS message from APDU field into a SMS field
            int SMS[] = new int[LenMessage + 1];
            for (n = 1; n <= LenMessage; n++) {
                // convertion: unsigned byte -> integer
                SMS[n] = (0x000000FF & RspAPDU[MessageStart + n]);
            } // for
            //----- convert the whole SMS record
            Bytebefore = 0;
            for (n = 1; n <= LenMessage; n++) {
                x = SMS[n] ;                    // get a byte from the SMS
                Bitshift = (n-1) % 7;           // calculate number of neccssary bit shif
                y = x;
                y = y << Bitshift;              // shift to get a conversion 7 bit compac
                y = y | Bytebefore;             // add bits from the byte before this byt
                y = y & 0x0000007F;             // delete all bits except bit 7 ... 1 of
//                s = s + convertGSM2Unicode(y); // conversion: 7 bit GSM character -> Jav
                s = s + GSMCardUtils.byteToChar((byte) y);
                if (Bitshift == 6) {
                    Bitshift = 1;
                    y = x;
                    y = y >>> Bitshift;             // shift to get a conversion 7 bit compa
                    y = y & 0x0000007F;             // delete all bits except bit 7 ... 1 of
//                    s = s + convertGSM2Unicode(y); // conversion: 7 bit GSM character -> Jav
                    s = s + GSMCardUtils.byteToChar((byte) y);
                    Bytebefore = 0;
                } // if
                else {
                    Bytebefore = x;
                    Bitshift = 7 - Bitshift;
                    Bytebefore = Bytebefore >>> Bitshift; // shift to get a conversion 7 b
                    Bytebefore = Bytebefore & 0x000000FF; // mask for one byte
                } // else
            } // for EndOfSms
        } // if
        return s;
    } // decodeSMSRecord
    
    
}
